package com.lbd.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.lbd.common.constrant.ProductConstrant;
import com.lbd.gulimall.product.service.CategoryBrandRelationService;
import com.lbd.gulimall.product.vo.Catalog2VO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lbd.common.utils.PageUtils;
import com.lbd.common.utils.Query;

import com.lbd.gulimall.product.dao.CategoryDao;
import com.lbd.gulimall.product.entity.CategoryEntity;
import com.lbd.gulimall.product.service.CategoryService;
import org.springframework.util.StringUtils;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> queryTree() {
        List<CategoryEntity> categorys = baseMapper.selectList(null);
        List<CategoryEntity> dataTree = categorys.stream()
                //获得所有的顶级ID
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                //为顶级ID设置子分类
                .map(categoryEntity -> {
                    categoryEntity.setChildCategoryEntity(getChildList(categorys, categoryEntity));
                    return categoryEntity;
                })
                //排序。数值小的优先，反之2 - 1
                .sorted((category1, category2) -> {
                    return (category1.getSort() == null ? 0 : category1.getSort())
                            - (category2.getSort() == null ? 0 : category2.getSort());
                })
                .collect(Collectors.toList());
        return dataTree;
    }

    @Override
    public void removeCategoryByIds(Long[] catIds) {
        //TODO //1. 被引用到的分类不能删除
        //2. 应该做逻辑删除
        baseMapper.deleteBatchIds(Arrays.asList(catIds));
    }

    @Override
    public Long[] getCatelogPath(Long catId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parents = getParents(catId, paths);
        Collections.reverse(parents);
        return parents.toArray(new Long[]{});
    }

    @CacheEvict(value = "category",allEntries = true)
    @Override
    public void updateDetail(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategoryInfo(category.getCatId(), category.getName());
    }

    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    @Override
    public List<CategoryEntity> getLevel1() {
        log.info("从数据库中查询一级分类...");
        QueryWrapper<CategoryEntity> categoryEntityQueryWrapper = new QueryWrapper<>();
        categoryEntityQueryWrapper.eq("parent_cid", 0);
        return baseMapper.selectList(categoryEntityQueryWrapper);
    }

    @Override
    public void getCached() {
        this.getLevel1();
    }

    public List<CategoryEntity> getCategoryByParentCid(List<CategoryEntity> allCategories, Long parentCid) {
        return allCategories
                .stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == parentCid)
                .collect(Collectors.toList());
    }

    //使用SpringCache简化缓存开发
    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    @Override
    public Map<String, List<Catalog2VO>> getCategoryInfo() {
        log.info("从数据库中查询二三级分类...");
        Map<String, List<Catalog2VO>> category = getCategory();
        return category;
    }


    //自定义缓存实现逻辑
    public Map<String, List<Catalog2VO>> CustomCacheImpl() {
        Map<String, List<Catalog2VO>> collect;
        String category = stringRedisTemplate.opsForValue().get(ProductConstrant.CATALOG_KEY);
        if (StringUtils.isEmpty(category)) {
            log.info("缓存未命中，准备查询数据库");
            //存在缓存击穿问题，需要加分布式锁，只让一个线程去数据库获取值，其余线程空转等待缓存有值后再去缓存中读取
            collect = getCategoryForDB();
        } else {
            log.info("缓存命中，使用缓存中的值");
            collect = JSON.parseObject(category, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
        }
        return collect;
    }

    //存在缓存击穿问题，需要加分布式锁，只让一个线程去数据库获取值，其余线程空转等待缓存有值后再去缓存中读取
    private Map<String, List<Catalog2VO>> getCategoryForDB() {
        String uuid = UUID.randomUUID().toString();
        //加锁和设置超时时间，需要保证原子性，防止死锁
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(ProductConstrant.CATALOG_LOCK_KEY, uuid, ProductConstrant.CATALOG_LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
        if (isLock) {
            Map<String, List<Catalog2VO>> collect = null;
            try {
                //在获取到锁的时候需要再次查询下缓存中是否有值
                String category = stringRedisTemplate.opsForValue().get(ProductConstrant.CATALOG_KEY);
                if (!StringUtils.isEmpty(category)) {
                    //查询到空值，可能是恶意请求，返回空map
                    return ("NULL".equals(category)) ? new HashMap<>() : JSON.parseObject(category, new TypeReference<Map<String, List<Catalog2VO>>>() {
                    });
                }
                log.info("开始查询数据库...");
                collect = getCategory();
                //为防止缓存穿透问题，需要将null值缓存
                String categoryInfo = (collect == null || collect.isEmpty()) ? "NULL" : JSON.toJSONString(collect);
                //写缓存操作必须在加锁的逻辑中执行。否则会出现，A线程未将查询数据库结果写缓存前，B线程获取缓存为空，也去数据库查询，导致没锁柱
                //TODO: 为了防止缓存雪崩，对于存储redis的商品数据应该用随机值作为过期时间，防止同一时刻大量key失效，导致所有流量打到DB
                stringRedisTemplate.opsForValue().set(ProductConstrant.CATALOG_KEY, categoryInfo, ProductConstrant.CATALOG_EXPIRE_TIME, TimeUnit.SECONDS);
            } finally {
                //比对锁和释放锁需要是原子操作，防止误删了其他线程设置的锁
                String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                Long execute = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList(ProductConstrant.CATALOG_LOCK_KEY), uuid);
            }
            return collect;
        } else {
            //未获取到锁的线程，自旋的获取锁
            log.info("获取锁失败，等待重试...");
            try {
                //通过休眠防止栈溢出
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                log.error(e.getMessage());
            }
            return getCategoryForDB();
        }
    }

    private Map<String, List<Catalog2VO>> getCategory() {
        //查询全部数据，在内存中过滤
        List<CategoryEntity> allCategories = baseMapper.selectList(null);
        List<CategoryEntity> level1s = getCategoryByParentCid(allCategories,0l);
        return level1s.stream().collect(Collectors.toMap(key -> key.getCatId().toString(), level1 -> {
            Long level1CatId = level1.getCatId();
            List<CategoryEntity> level2s = getCategoryByParentCid(allCategories, level1CatId);
            List<Catalog2VO> catalog2VOS = level2s.stream().map(level2 -> {
                Long level2CatId = level2.getCatId();
                Catalog2VO catalog2VO = new Catalog2VO(level1CatId, level2CatId, level2.getName(), null);
                List<CategoryEntity> level3s = getCategoryByParentCid(allCategories, level2CatId);
                List<Catalog2VO.Catalog3VO> catalog3VOS = level3s.stream().map(level3 -> {
                    Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(level2CatId, level3.getCatId(), level3.getName());
                    return catalog3VO;
                }).collect(Collectors.toList());
                catalog2VO.setCatalog3List(catalog3VOS);
                return catalog2VO;
            }).collect(Collectors.toList());
            return catalog2VOS;
        }));
    }

    //递归获取父分类
    private List<Long> getParents(Long catId, List<Long> paths) {
        CategoryEntity categoryEntity = this.getById(catId);
        if (categoryEntity != null) {
            Long parentCid = categoryEntity.getParentCid();
            if (parentCid != null || parentCid != 0) {
                paths.add(catId);
                getParents(parentCid, paths);
            }
        }
        return paths;
    }


    /**
     * 递归获取子分类信息
     *
     * @param all:  所有分类信息
     * @param root: 当前分类信息
     * @return:
     * @author: linbd
     * @date: 2021/6/9 16:49
     */
    public List<CategoryEntity> getChildList(List<CategoryEntity> all, CategoryEntity root) {
        List<CategoryEntity> childList = all.stream()
                //建立层级关系，如果当前菜单的父ID等于传入分类的id，则满足筛选条件
                .filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
                //由于子分类可能还有子分类，所以需要递归的设置子分类。
                //如果没有这个map，则表现为只有一层子分类。每次递归的变量为categoryEntity
                .map(categoryEntity -> {
                    categoryEntity.setChildCategoryEntity(getChildList(all, categoryEntity));
                    return categoryEntity;
                })
                .sorted((category1, category2) -> {
                    return (category1.getSort() == null ? 0 : category1.getSort())
                            - (category2.getSort() == null ? 0 : category2.getSort());
                })
                .collect(Collectors.toList());
        return childList;
    }

}